package com.immediatus.engine;

import com.immediatus.contracts.IUpdateHandler;
import com.immediatus.engine.contracts.IUnit;
import com.immediatus.engine.contracts.IUnitModifier;
import com.immediatus.engine.modifier.UnitModifierList;
import com.immediatus.engine.util.ParameterCallable;
import com.immediatus.engine.util.Transformation;
import com.immediatus.engine.util.ZIndexSorter;
import com.immediatus.graphics.camera.Camera;
import com.immediatus.util.SmartList;
import com.immediatus.util.UpdateHandlerList;

import javax.microedition.khronos.opengles.GL10;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

import static com.immediatus.engine.contracts.IUnitModifier.IEntityModifierMatcher;

public class Unit implements IUnit{
    public static final int VERTEX_INDEX_X = 0;
    public static final int VERTEX_INDEX_Y = 1;

    private static final int CHILDREN_CAPACITY_DEFAULT = 4;
    private static final int ENTITYMODIFIERS_CAPACITY_DEFAULT = 4;
    private static final int UPDATEHANDLERS_CAPACITY_DEFAULT = 4;

    private static final float[] VERTICES_SCENE_TO_LOCAL_TMP = new float[2];
    private static final float[] VERTICES_LOCAL_TO_SCENE_TMP = new float[2];

    private static final ParameterCallable<IUnit> _DETACHCHILD = new ParameterCallable<IUnit>(){
        @Override
        public void call(final IUnit unit_){
            unit_.setParent(null);
            unit_.onDetached();
        }
    };

    private boolean _visible = true;
    private boolean _ignoreUpdate = false;
    private boolean _childrenVisible = true;
    private boolean _childrenIgnoreUpdate = false;
    private int _zIndex = 0;

    private SmartList<IUnit> _children;
    private float _red = 1f;
    private float _green = 1f;
    private float _blue = 1f;
    private float _alpha = 1f;

    private float _x;
    private float _y;

    private final float _initialX;
    private final float _initialY;

    private float _rotation = 0;

    private float _rotationCenterX = 0;
    private float _rotationCenterY = 0;

    private float _scaleX = 1f;
    private float _scaleY = 1f;

    private float _scaleCenterX = 0;
    private float _scaleCenterY = 0;

    private boolean _localToParentTransformationDirty = true;
    private boolean _parentToLocalTransformationDirty = true;

    private final Transformation _localToParentTransformation = new Transformation();
    private final Transformation _parentToLocalTransformation = new Transformation();

    private final Transformation _localToLayerTransformation = new Transformation();
    private final Transformation _layerToLocalTransformation = new Transformation();

    private Object _userData;
    private IUnit _parent;
    private UnitModifierList _unitModifiers;
    private UpdateHandlerList _updateHandlers;

    public Unit(){
        this(0, 0);
    }

    public Unit(final float x_, final float y_){
        this._initialX = x_;
        this._initialY = y_;

        this._x = x_;
        this._y = y_;
    }

    @Override
    public boolean isVisible(){
        return this._visible;
    }

    @Override
    public void setVisible(final boolean visible_){
        this._visible = visible_;
    }

    @Override
    public boolean isChildrenVisible(){
        return this._childrenVisible;
    }

    @Override
    public void setChildrenVisible(final boolean childrenVisible_){
        this._childrenVisible = childrenVisible_;
    }

    @Override
    public boolean isIgnoreUpdate(){
        return this._ignoreUpdate;
    }

    @Override
    public void setIgnoreUpdate(final boolean ignoreUpdate_){
        this._ignoreUpdate = ignoreUpdate_;
    }

    @Override
    public boolean isChildrenIgnoreUpdate(){
        return this._childrenIgnoreUpdate;
    }

    @Override
    public void setChildrenIgnoreUpdate(final boolean childrenIgnoreUpdate_){
        this._childrenIgnoreUpdate = childrenIgnoreUpdate_;
    }

    @Override
    public boolean hasParent(){
        return this._parent != null;
    }

    @Override
    public IUnit getParent(){
        return this._parent;
    }

    @Override
    public void setParent(final IUnit unit_){
        this._parent = unit_;
    }

    @Override
    public int getZIndex(){
        return this._zIndex;
    }

    @Override
    public void setZIndex(final int zIndex_){
        this._zIndex = zIndex_;
    }

    @Override
    public float getX(){
        return this._x;
    }

    @Override
    public float getY(){
        return this._y;
    }

    @Override
    public float getInitialX(){
        return this._initialX;
    }

    @Override
    public float getInitialY(){
        return this._initialY;
    }

    @Override
    public void setPosition(final IUnit unit_){
        this.setPosition(unit_.getX(), unit_.getY());
    }

    @Override
    public void setPosition(final float x, final float y){
        this._x = x;
        this._y = y;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public void setInitialPosition(){
        this._x = this._initialX;
        this._y = this._initialY;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public float getRotation(){
        return this._rotation;
    }

    @Override
    public boolean isRotated(){
        return this._rotation != 0;
    }

    @Override
    public void setRotation(final float rotation_){
        this._rotation = rotation_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public float getRotationCenterX(){
        return this._rotationCenterX;
    }

    @Override
    public float getRotationCenterY(){
        return this._rotationCenterY;
    }

    @Override
    public void setRotationCenterX(final float x_){
        this._rotationCenterX = x_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public void setRotationCenterY(final float y_){
        this._rotationCenterY = y_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public void setRotationCenter(final float x_, final float y_){
        this._rotationCenterX = x_;
        this._rotationCenterY = y_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public boolean isScaled(){
        return this._scaleX != 1 || this._scaleY != 1;
    }

    @Override
    public float getScaleX(){
        return this._scaleX;
    }

    @Override
    public float getScaleY(){
        return this._scaleY;
    }

    @Override
    public void setScaleX(final float sx_){
        this._scaleX = sx_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public void setScaleY(final float sy_){
        this._scaleY = sy_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public void setScale(final float s_){
        this._scaleX = s_;
        this._scaleY = s_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public void setScale(final float sx_, final float sy_){
        this._scaleX = sx_;
        this._scaleY = sy_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public float getScaleCenterX(){
        return this._scaleCenterX;
    }

    @Override
    public float getScaleCenterY(){
        return this._scaleCenterY;
    }

    @Override
    public void setScaleCenterX(final float x_){
        this._scaleCenterX = x_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public void setScaleCenterY(final float y_){
        this._scaleCenterY = y_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public void setScaleCenter(final float x_, final float y_){
        this._scaleCenterX = x_;
        this._scaleCenterY = y_;
        this._localToParentTransformationDirty = true;
        this._parentToLocalTransformationDirty = true;
    }

    @Override
    public float getRed(){
        return this._red;
    }

    @Override
    public float getGreen(){
        return this._green;
    }

    @Override
    public float getBlue(){
        return this._blue;
    }

    @Override
    public float getAlpha(){
        return this._alpha;
    }

    @Override
    public void setAlpha(final float a_){
        this._alpha = a_;
    }

    @Override
    public void setColor(final float r_, final float g_, final float b_){
        this._red = r_;
        this._green = g_;
        this._blue = b_;
    }

    @Override
    public void setColor(final float r_, final float g_, final float b_, final float a_){
        this._red = r_;
        this._green = g_;
        this._blue = b_;
        this._alpha = a_;
    }

    @Override
    public int getChildCount(){
        if (this._children == null){
            return 0;
        }
        return this._children.size();
    }

    @Override
    public IUnit getChild(final int index_){
        if (this._children == null){
            return null;
        }
        return this._children.get(index_);
    }

    @Override
    public int getChildIndex(final IUnit unit_){
        if (this._children == null || unit_.getParent() != this){
            return -1;
        }
        return this._children.indexOf(unit_);
    }

    @Override
    public boolean setChildIndex(final IUnit unit_, final int index_){
        if (this._children == null || unit_.getParent() != this){
            return false;
        }
        try{
            this._children.remove(unit_);
            this._children.add(index_, unit_);
            return true;
        } catch (final IndexOutOfBoundsException e){
            return false;
        }
    }

    @Override
    public IUnit getFirstChild(){
        if (this._children == null){
            return null;
        }
        return this._children.get(0);
    }

    @Override
    public IUnit getLastChild(){
        if (this._children == null){
            return null;
        }
        return this._children.get(this._children.size() - 1);
    }

    @Override
    public boolean detachSelf(){
        final IUnit parent = this._parent;
        return parent != null && parent.detachChild(this);
    }

    @Override
    public void detachChildren(){
        if (this._children == null){
            return;
        }
        this._children.clear(_DETACHCHILD);
    }

    @Override
    public void attachChild(final IUnit unit_) throws IllegalStateException{
        if (unit_.hasParent()){
            throw new IllegalStateException("pEntity already has a parent!");
        }
        if (this._children == null){
            this.allocateChildren();
        }
        this._children.add(unit_);
        unit_.setParent(this);
        unit_.onAttached();
    }

    @Override
    public boolean attachChild(final IUnit unit_, final int index_) throws IllegalStateException{
        if (unit_.hasParent()){
            throw new IllegalStateException("Unit already has a parent");
        }
        if (this._children == null){
            this.allocateChildren();
        }
        try{
            this._children.add(index_, unit_);
            unit_.setParent(this);
            unit_.onAttached();
            return true;
        } catch (final IndexOutOfBoundsException e){
            return false;
        }
    }

    @Override
    public IUnit findChild(final IEntityMatcher matcher_){
        if (this._children == null){
            return null;
        }
        return this._children.find(matcher_);
    }

    @Override
    public boolean swapChildren(final IUnit unitA_, final IUnit unitB_){
        return this.swapChildren(this.getChildIndex(unitA_), this.getChildIndex(unitB_));
    }

    @Override
    public boolean swapChildren(final int indexA_, final int indexB_){
        try{
            Collections.swap(this._children, indexA_, indexB_);
            return true;
        } catch (final IndexOutOfBoundsException e){
            return false;
        }
    }

    @Override
    public void sortChildren(){
        if (this._children == null){
            return;
        }
        ZIndexSorter.getInstance().sort(this._children);
    }

    @Override
    public void sortChildren(final Comparator<IUnit> comparator_){
        if (this._children == null){
            return;
        }
        ZIndexSorter.getInstance().sort(this._children, comparator_);
    }

    @Override
    public boolean detachChild(final IUnit unit_){
        return this._children != null && this._children.remove(unit_, _DETACHCHILD);
    }

    @Override
    public IUnit detachChild(final IEntityMatcher matcher_){
        if (this._children == null){
            return null;
        }
        return this._children.remove(matcher_, Unit._DETACHCHILD);
    }

    @Override
    public boolean detachChildren(final IEntityMatcher matcher_){
        return this._children != null && this._children.removeAll(matcher_, Unit._DETACHCHILD);
    }

    @Override
    public void callOnChildren(final IEntityCallable callable_){
        if (this._children == null){
            return;
        }
        this._children.call(callable_);
    }

    @Override
    public void callOnChildren(final IEntityMatcher matcher_, final IEntityCallable callable_){
        if (this._children == null){
            return;
        }
        this._children.call(matcher_, callable_);
    }

    @Override
    public void registerUpdateHandler(final IUpdateHandler handler_){
        if (this._updateHandlers == null){
            this.allocateUpdateHandlers();
        }
        this._updateHandlers.add(handler_);
    }

    @Override
    public boolean unregisterUpdateHandler(final IUpdateHandler handler_){
        return this._updateHandlers != null && this._updateHandlers.remove(handler_);
    }

    @Override
    public boolean unregisterUpdateHandlers(final IUpdateHandlerMatcher matcher_){
        return this._updateHandlers != null && this._updateHandlers.removeAll(matcher_);
    }

    @Override
    public void clearUpdateHandlers(){
        if (this._updateHandlers == null){
            return;
        }
        this._updateHandlers.clear();
    }

    @Override
    public void registerEntityModifier(final IUnitModifier modifier_){
        if (this._unitModifiers == null){
            this.allocateEntityModifiers();
        }
        this._unitModifiers.add(modifier_);
    }

    @Override
    public boolean unregisterEntityModifier(final IUnitModifier modifier_){
        return this._unitModifiers != null && this._unitModifiers.remove(modifier_);
    }

    @Override
    public boolean unregisterEntityModifiers(final IEntityModifierMatcher matcher_){
        return this._unitModifiers != null && this._unitModifiers.removeAll(matcher_);
    }

    @Override
    public void clearEntityModifiers(){
        if (this._unitModifiers == null){
            return;
        }
        this._unitModifiers.clear();
    }

    @Override
    public float[] getLayerCenterCoordinates(){
        return this.convertLocalToLayerCoordinates(0, 0);
    }

    public Transformation getLocalToParentTransformation(){
        final Transformation localToParentTransformation = this._localToParentTransformation;
        if (this._localToParentTransformationDirty){
            localToParentTransformation.setToIdentity();

            final float scaleX = this._scaleX;
            final float scaleY = this._scaleY;
            if (scaleX != 1 || scaleY != 1){
                final float scaleCenterX = this._scaleCenterX;
                final float scaleCenterY = this._scaleCenterY;
                localToParentTransformation.postTranslate(-scaleCenterX, -scaleCenterY);
                localToParentTransformation.postScale(scaleX, scaleY);
                localToParentTransformation.postTranslate(scaleCenterX, scaleCenterY);
            }

            final float rotation = this._rotation;
            if (rotation != 0){
                final float rotationCenterX = this._rotationCenterX;
                final float rotationCenterY = this._rotationCenterY;
                localToParentTransformation.postTranslate(-rotationCenterX, -rotationCenterY);
                localToParentTransformation.postRotate(rotation);
                localToParentTransformation.postTranslate(rotationCenterX, rotationCenterY);
            }
            localToParentTransformation.postTranslate(this._x, this._y);
            this._localToParentTransformationDirty = false;
        }
        return localToParentTransformation;
    }

    public Transformation getParentToLocalTransformation(){
        final Transformation parentToLocalTransformation = this._parentToLocalTransformation;
        if (this._parentToLocalTransformationDirty){
            parentToLocalTransformation.setToIdentity();
            parentToLocalTransformation.postTranslate(-this._x, -this._y);
            final float rotation = this._rotation;
            if (rotation != 0){
                final float rotationCenterX = this._rotationCenterX;
                final float rotationCenterY = this._rotationCenterY;
                parentToLocalTransformation.postTranslate(-rotationCenterX, -rotationCenterY);
                parentToLocalTransformation.postRotate(-rotation);
                parentToLocalTransformation.postTranslate(rotationCenterX, rotationCenterY);
            }
            final float scaleX = this._scaleX;
            final float scaleY = this._scaleY;
            if (scaleX != 1 || scaleY != 1){
                final float scaleCenterX = this._scaleCenterX;
                final float scaleCenterY = this._scaleCenterY;
                parentToLocalTransformation.postTranslate(-scaleCenterX, -scaleCenterY);
                parentToLocalTransformation.postScale(1 / scaleX, 1 / scaleY);
                parentToLocalTransformation.postTranslate(scaleCenterX, scaleCenterY);
            }
            this._parentToLocalTransformationDirty = false;
        }
        return parentToLocalTransformation;
    }

    @Override
    public Transformation getLocalToLayerTransformation(){
        final Transformation localToLayerTransformation = this._localToLayerTransformation;
        localToLayerTransformation.setTo(this.getLocalToParentTransformation());

        final IUnit parent = this._parent;
        if (parent != null){
            localToLayerTransformation.postConcat(parent.getLocalToLayerTransformation());
        }

        return localToLayerTransformation;
    }

    @Override
    public Transformation getLayerToLocalTransformation(){
        final Transformation layerToLocalTransformation = this._layerToLocalTransformation;
        layerToLocalTransformation.setTo(this.getParentToLocalTransformation());

        final IUnit parent = this._parent;
        if (parent != null){
            layerToLocalTransformation.postConcat(parent.getLayerToLocalTransformation());
        }

        return layerToLocalTransformation;
    }

    @Override
    public float[] convertLocalToLayerCoordinates(final float x_, final float y_){
        return this.convertLocalToLayerCoordinates(x_, y_, VERTICES_LOCAL_TO_SCENE_TMP);
    }

    @Override
    public float[] convertLocalToLayerCoordinates(final float x_, final float y_, final float[] reuse_){
        reuse_[VERTEX_INDEX_X] = x_;
        reuse_[VERTEX_INDEX_Y] = y_;
        this.getLocalToLayerTransformation().transform(reuse_);
        return reuse_;
    }

    @Override
    public float[] convertLocalToLayerCoordinates(final float[] coord_){
        return this.convertLayerToLocalCoordinates(coord_, VERTICES_LOCAL_TO_SCENE_TMP);
    }

    @Override
    public float[] convertLocalToLayerCoordinates(final float[] coord_, final float[] reuse_){
        reuse_[VERTEX_INDEX_X] = coord_[VERTEX_INDEX_X];
        reuse_[VERTEX_INDEX_Y] = coord_[VERTEX_INDEX_Y];
        this.getLocalToLayerTransformation().transform(reuse_);
        return reuse_;
    }

    @Override
    public float[] convertLayerToLocalCoordinates(final float x_, final float y_){
        return this.convertLayerToLocalCoordinates(x_, y_, VERTICES_SCENE_TO_LOCAL_TMP);
    }

    @Override
    public float[] convertLayerToLocalCoordinates(final float x_, final float y_, final float[] reuse_){
        reuse_[VERTEX_INDEX_X] = x_;
        reuse_[VERTEX_INDEX_Y] = y_;
        this.getLayerToLocalTransformation().transform(reuse_);
        return reuse_;
    }

    @Override
    public float[] convertLayerToLocalCoordinates(final float[] coord_){
        return this.convertLayerToLocalCoordinates(coord_, VERTICES_SCENE_TO_LOCAL_TMP);
    }

    @Override
    public float[] convertLayerToLocalCoordinates(final float[] coord_, final float[] reuse_){
        reuse_[VERTEX_INDEX_X] = coord_[VERTEX_INDEX_X];
        reuse_[VERTEX_INDEX_Y] = coord_[VERTEX_INDEX_Y];
        this.getLayerToLocalTransformation().transform(reuse_);
        return reuse_;
    }

    @Override
    public void onAttached(){

    }

    @Override
    public void onDetached(){

    }

    @Override
    public Object getUserData(){
        return this._userData;
    }

    @Override
    public void setUserData(final Object userData_){
        this._userData = userData_;
    }

    @Override
    public final void onDraw(final GL10 gl_, final Camera camera_){
        if (this._visible){
            this.onManagedDraw(gl_, camera_);
        }
    }

    @Override
    public final void onUpdate(final float secondsElapsed_){
        if (!this._ignoreUpdate){
            this.onManagedUpdate(secondsElapsed_);
        }
    }

    @Override
    public void reset(){
        this._visible = true;
        this._ignoreUpdate = false;
        this._childrenVisible = true;
        this._childrenIgnoreUpdate = false;

        this._x = this._initialX;
        this._y = this._initialY;
        this._rotation = 0;
        this._scaleX = 1;
        this._scaleY = 1;

        this._red = 1.0f;
        this._green = 1.0f;
        this._blue = 1.0f;
        this._alpha = 1.0f;

        if (this._unitModifiers != null){
            this._unitModifiers.reset();
        }

        if (this._children != null){
            final ArrayList<IUnit> units = this._children;
            for (int i = units.size() - 1; i >= 0; i--){
                units.get(i).reset();
            }
        }
    }

    public void onManagedDrawChildren(final GL10 gl_, final Camera camera_){
        final ArrayList<IUnit> children = this._children;
        for (IUnit aChildren : children)
            aChildren.onDraw(gl_, camera_);
    }

    protected void doDraw(final GL10 gl_, final Camera camera_){
    }

    private void allocateEntityModifiers(){
        this._unitModifiers = new UnitModifierList(this, Unit.ENTITYMODIFIERS_CAPACITY_DEFAULT);
    }

    private void allocateChildren(){
        this._children = new SmartList<IUnit>(Unit.CHILDREN_CAPACITY_DEFAULT);
    }

    private void allocateUpdateHandlers(){
        this._updateHandlers = new UpdateHandlerList(Unit.UPDATEHANDLERS_CAPACITY_DEFAULT);
    }

    protected void onApplyTransformations(final GL10 gl_){
        this.applyTranslation(gl_);
        this.applyRotation(gl_);
        this.applyScale(gl_);
    }

    protected void applyTranslation(final GL10 gl_){
        gl_.glTranslatef(this._x, this._y, 0);
    }

    protected void applyRotation(final GL10 gl_){
        final float rotation = this._rotation;

        if (rotation != 0){
            final float rotationCenterX = this._rotationCenterX;
            final float rotationCenterY = this._rotationCenterY;

            gl_.glTranslatef(rotationCenterX, rotationCenterY, 0);
            gl_.glRotatef(rotation, 0, 0, 1);
            gl_.glTranslatef(-rotationCenterX, -rotationCenterY, 0);
        }
    }

    protected void applyScale(final GL10 gl_){
        final float scaleX = this._scaleX;
        final float scaleY = this._scaleY;

        if (scaleX != 1 || scaleY != 1){
            final float scaleCenterX = this._scaleCenterX;
            final float scaleCenterY = this._scaleCenterY;

            gl_.glTranslatef(scaleCenterX, scaleCenterY, 0);
            gl_.glScalef(scaleX, scaleY, 1);
            gl_.glTranslatef(-scaleCenterX, -scaleCenterY, 0);
        }
    }

    protected void onManagedDraw(final GL10 gl_, final Camera camera_){
        gl_.glPushMatrix();
        {
            this.onApplyTransformations(gl_);
            this.doDraw(gl_, camera_);
            this.onDrawChildren(gl_, camera_);
        }
        gl_.glPopMatrix();
    }

    protected void onDrawChildren(final GL10 gl_, final Camera camera_){
        if (this._children != null && this._childrenVisible){
            this.onManagedDrawChildren(gl_, camera_);
        }
    }

    protected void onManagedUpdate(final float secondsElapsed_){
        if (this._unitModifiers != null){
            this._unitModifiers.onUpdate(secondsElapsed_);
        }
        if (this._updateHandlers != null){
            this._updateHandlers.onUpdate(secondsElapsed_);
        }

        if (this._children != null && !this._childrenIgnoreUpdate){
            final ArrayList<IUnit> units = this._children;
            for (IUnit unit : units)
                unit.onUpdate(secondsElapsed_);
        }
    }
}
